package bg.btanev.eightyfour.gwtfacade.converter;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Map;

import org.apache.commons.lang.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author B.Tanev
 *
 */
public final class ReflectionUtils {
	/**
	 * Logger for this class
	 */
	private static final Logger logger = LoggerFactory.getLogger(ReflectionUtils.class);

	private final ConverterConfiguration config;

	public ReflectionUtils() {
		this(null);
	}

	public ReflectionUtils(ConverterConfiguration config) {
		this.config = config;
	}

	public <T> T initClass(Class<T> clazz) {
		Validate.notNull(clazz);

		T instance = null;
		try {
			Constructor<T> constructor = clazz.getDeclaredConstructor();
			if (!(Modifier.isPublic(constructor.getModifiers()))) {
				constructor.setAccessible(true);
			}
			instance = constructor.newInstance();
		} catch (SecurityException e) {
			logger.warn("initClass(Class<T>) - Could not init class: " + clazz, e); //$NON-NLS-1$
		} catch (NoSuchMethodException e) {
			logger.warn("initClass(Class<T>) - Could not init class: " + clazz, e); //$NON-NLS-1$
		} catch (IllegalArgumentException e) {
			logger.warn("initClass(Class<T>) - Could not init class: " + clazz, e); //$NON-NLS-1$
		} catch (InstantiationException e) {
			logger.warn("initClass(Class<T>) - Could not init class: " + clazz, e); //$NON-NLS-1$
		} catch (IllegalAccessException e) {
			logger.warn("initClass(Class<T>) - Could not init class: " + clazz, e); //$NON-NLS-1$
		} catch (InvocationTargetException e) {
			logger.warn("initClass(Class<T>) - Could not init class: " + clazz, e); //$NON-NLS-1$
		}

		return instance;
	}

	public void copyObjectField(Object source, Object sink, int depth) {
		if (depth-- < 0) {
			return;
		}

		Validate.notNull(source);
		Validate.notNull(sink);

		Class<?> sinkClazz = sink.getClass();
		Class<?> sourceClazz = source.getClass();
		if (sourceClazz.getName().startsWith("net.sf.cglib.")) {
			return;
		}

		populateFields(source, sink, sourceClazz, sinkClazz, depth);
	}

	@SuppressWarnings("unchecked")
	private void populateFields(final Object source, final Object sink, final Class<?> sourceClazz,
			final Class<?> sinkClazz, final int depth) {

		assert (source != null);
		assert (sink != null);
		assert (sourceClazz != null);
		assert (sinkClazz != null);
		assert (depth >= -1);

		Class<?> sourceSuperclazz = sourceClazz.getSuperclass();
		Class<?> sinkSuperclazz = sinkClazz.getSuperclass();
		if (sourceSuperclazz != null && sinkSuperclazz != null) {
			populateFields(source, sink, sourceSuperclazz, sinkSuperclazz, depth);
		}

		Field[] sourceFields = sourceClazz.getDeclaredFields();
		for (Field field : sourceFields) {
			boolean process = canProcessField(field);

			if (!process) {
				continue;
			}

			int modifiers = field.getModifiers();
			if (!(Modifier.isPublic(modifiers))) {
				field.setAccessible(true);
			}

			Class fieldClazz = field.getType();
			String fieldName = field.getName();
			try {
				Object fieldValue = null;

				if (isPrimitiveType(fieldClazz)) {
					if (Boolean.TYPE.equals(fieldClazz)) {
						fieldValue = Boolean.valueOf(field.getBoolean(source));
					} else if (Byte.TYPE.equals(fieldClazz)) {
						fieldValue = Byte.valueOf(field.getByte(source));
					} else if (Character.TYPE.equals(fieldClazz)) {
						fieldValue = Character.valueOf(field.getChar(source));
					} else if (Float.TYPE.equals(fieldClazz)) {
						fieldValue = Float.valueOf(field.getFloat(source));
					} else if (Double.TYPE.equals(fieldClazz)) {
						fieldValue = Double.valueOf(field.getDouble(source));
					} else if (Integer.TYPE.equals(fieldClazz)) {
						fieldValue = Integer.valueOf(field.getInt(source));
					} else if (Long.TYPE.equals(fieldClazz)) {
						fieldValue = Long.valueOf(field.getLong(source));
					} else if (Short.TYPE.equals(fieldClazz)) {
						fieldValue = Short.valueOf(field.getShort(source));
					}

					setFieldValue(sink, sinkClazz, fieldName, fieldClazz, fieldValue);
				} else {
					// not primitive type

					/*
					 * trying to read a value from read method if available - ust in case there is a proxy wrapping this
					 * method
					 */
					fieldValue = valueFromReadMethod(source, fieldName);
					if (fieldValue == null) {
						fieldValue = field.get(source);
					}
					if (fieldValue != null) {
						// because of polymorphism
						fieldClazz = fieldValue.getClass();
					}

					Class toClazz = fieldClazz;

					Field sinkField = getClassField(sinkClazz, fieldName);
					if (sinkField != null) {
						Class<?> sinkFieldType = sinkField.getType();
						if (!(sinkFieldType.isAssignableFrom(toClazz))) {
							if (config != null) {
								if (fieldClazz.isArray()) {
									toClazz = config.getAssociation(fieldClazz.getComponentType());
								} else {
									toClazz = config.getAssociation(fieldClazz);
								}

								if (toClazz == null) {
									continue;
								}
							} else {
								continue;
							}
						}
					} else {
						continue;
					}

					if (fieldClazz.isArray()) {
						if (toClazz.isArray()) {
							toClazz = toClazz.getComponentType();
						}
						fieldValue = copyArray(fieldValue, toClazz, depth);
						if (fieldValue != null) {
							toClazz = fieldValue.getClass();
						}
					} else if (Collection.class.isAssignableFrom(fieldClazz)) {
						// TODO copy collection - not needed for now
						continue;
					} else if (Map.class.isAssignableFrom(fieldClazz)) {
						// TODO copy map - not needed for now
					} else {
						// non collection type of class
					}

					if (fieldValue != null) {
						if (!(toClazz.equals(fieldValue.getClass()))) {
							Object subSinkObject = null;
							if (!(toClazz.isInterface())) {
								if (config != null) {
									if (config.containsClass(toClazz)) {
										subSinkObject = initClass(toClazz);
										if (subSinkObject != null) {
											copyObjectField(fieldValue, subSinkObject, depth);
										}
									}
								}
							}

							if (subSinkObject != null) {
								fieldValue = subSinkObject;
							} else if (!(toClazz.isAssignableFrom(fieldValue.getClass()))) {
								fieldValue = null;
							} else {
								//
							}
						}

						setFieldValue(sink, sinkClazz, fieldName, toClazz, fieldValue);
					}
				}
			} catch (IllegalArgumentException e) {
				logger.warn("copyObjectField(Object, Object) - exception ignored", e); //$NON-NLS-1$
			} catch (IllegalAccessException e) {
				logger.warn("copyObjectField(Object, Object) - exception ignored", e); //$NON-NLS-1$
			}
		}
	}

	private Object valueFromReadMethod(final Object source, String fieldName) {
		assert (source != null);
		assert (fieldName != null && fieldName.length() > 0);

		Class<?> clazz = source.getClass();
		Method readMethod = findReadMethod(clazz, fieldName);

		Object value = null;
		if (readMethod != null) {
			if (Modifier.isStatic(readMethod.getModifiers())) {
				return null;
			}

			if (!(readMethod.isAccessible())) {
				readMethod.setAccessible(true);
			}

			try {
				value = readMethod.invoke(source, (Object[]) null);
			} catch (IllegalArgumentException e) {
				value = null;
			} catch (IllegalAccessException e) {
				value = null;
			} catch (InvocationTargetException e) {
				value = null;
			}
		}

		return value;
	}

	/**
	 * Trying to find read method using JavaBean standard.
	 *
	 * @param clazz
	 * @param fieldName
	 * @return the {@link Method} or <code>null</code>
	 */
	private Method findReadMethod(Class<?> clazz, String fieldName) {
		assert (clazz != null);
		assert (fieldName != null && fieldName.length() > 0);

		Method readMethod = null;

		char[] nameLetters = fieldName.toCharArray();
		nameLetters[0] = Character.toUpperCase(nameLetters[0]);
		String propNameSuffix = new String(nameLetters);

		try {
			readMethod = clazz.getMethod("get" + propNameSuffix, (Class[]) null);
		} catch (SecurityException e) {
			readMethod = null;
		} catch (NoSuchMethodException e) {
			readMethod = null;
		}

		if (readMethod == null) {
			try {
				readMethod = clazz.getMethod("is" + propNameSuffix, (Class[]) null);
			} catch (SecurityException e) {
				readMethod = null;
			} catch (NoSuchMethodException e) {
				readMethod = null;
			}
		}

		return readMethod;
	}

	public Object copyArray(Object sourceArray, Class<?> resultType, int depth) {
		Validate.notNull(sourceArray);

		Class<?> sourceClazz = sourceArray.getClass();
		if (!sourceClazz.isArray()) {
			throw new IllegalArgumentException("Parameter is not an array.");
		}

		int length = Array.getLength(sourceArray);
		Object sinkArray = Array.newInstance(resultType, length);
		if (isPrimitiveType(sourceClazz.getComponentType())) {
			System.arraycopy(sourceArray, 0, sinkArray, 0, length);
		} else {
			for (int i = 0; i < length; i++) {
				Object resultTypeInstance = initClass(resultType);
				copyObjectField(((Object[]) sourceArray)[i], resultTypeInstance, depth);
				((Object[]) sinkArray)[i] = resultTypeInstance;
			}
		}

		return sinkArray;
	}

	@SuppressWarnings("unchecked")
	public void setFieldValue(Object object, Class<?> objectClazz, String name, Class typeClazz, Object value) {
		Validate.notNull(object);
		Validate.notNull(objectClazz);
		Validate.notEmpty(name);
		Validate.notNull(typeClazz);

		if (value == null) {
			logger.debug("setFieldValue(Object, String, Class<?>, Object) - end"); //$NON-NLS-1$
			return;
		}

		Field field = getClassField(objectClazz, name);
		if (field == null) {
			return;
		}

		try {
			if (field.getType().isAssignableFrom(typeClazz)) {
				if (isPrimitiveType(typeClazz)) {
					if (value == null) {
						return;
					}

					if (Boolean.TYPE.equals(typeClazz) && value instanceof Boolean) {
						field.setBoolean(object, ((Boolean) value).booleanValue());
					} else if (Byte.TYPE.equals(typeClazz) && value instanceof Byte) {
						field.setByte(object, ((Byte) value).byteValue());
					} else if (Character.TYPE.equals(typeClazz) && value instanceof Character) {
						field.setChar(object, ((Character) value).charValue());
					} else if (Float.TYPE.equals(typeClazz) && value instanceof Float) {
						field.setFloat(object, ((Float) value).floatValue());
					} else if (Double.TYPE.equals(typeClazz) && value instanceof Double) {
						field.setDouble(object, ((Double) value).doubleValue());
					} else if (Integer.TYPE.equals(typeClazz) && value instanceof Integer) {
						field.setInt(object, ((Integer) value).intValue());
					} else if (Long.TYPE.equals(typeClazz) && value instanceof Long) {
						field.setLong(object, ((Long) value).longValue());
					} else if (Short.TYPE.equals(typeClazz) && value instanceof Short) {
						field.setShort(object, ((Short) value).shortValue());
					}

				} else {
					field.set(object, value);
				}
			}
		} catch (SecurityException e) {
			logger.warn("setFieldValue(Object, String, Class<?>, Object) - exception ignored", e); //$NON-NLS-1$
		} catch (IllegalArgumentException e) {
			logger.warn("setFieldValue(Object, String, Class<?>, Object) - exception ignored", e); //$NON-NLS-1$
		} catch (IllegalAccessException e) {
			logger.warn("setFieldValue(Object, String, Class<?>, Object) - exception ignored", e); //$NON-NLS-1$
		}
	}

	public Field getClassField(Class<?> clazz, String name) {
		Validate.notNull(clazz);
		Validate.notEmpty(name);

		Field field = null;
		try {
			field = clazz.getDeclaredField(name);
		} catch (SecurityException e) {
			logger.warn("getClassField(Class<?>, String) - exception ignored", e); //$NON-NLS-1$
			return null;
		} catch (NoSuchFieldException e) {
			logger.warn("getClassField(Class<?>, String) - exception ignored", e); //$NON-NLS-1$
			return null;
		}
		if (!canProcessField(field)) {
			logger.debug("getClassField(Class<?>, String) - end"); //$NON-NLS-1$
			return null;
		}

		if (!(Modifier.isPublic(field.getModifiers()))) {
			field.setAccessible(true);
		}

		return field;
	}

	private boolean isPrimitiveType(Class<?> clazz) {
		assert (clazz != null);

		return clazz.isPrimitive();
	}

	private boolean canProcessField(Field field) {
		assert (field != null);

		int modifiers = field.getModifiers();

		boolean process = !Modifier.isFinal(modifiers);
		process = process && (!Modifier.isStatic(modifiers));
		process = process && (!Modifier.isTransient(modifiers));
		process = process && (!field.isSynthetic());

		return process;
	}
}
